我来给大家回答个问题
上一篇:
提出我在玩 Android开始更新问答了,当然也有同学留言。
不仅要有问答,还要有答案
说实话,太难办到了,因为什么呢?因为缺少时间。我现在每天大概就一个小时时间,23 点到 24 点,这一个小时主要用来看看文章,更新在网站文章以及问答。
不过偶尔回答一个问题还是可以的,我准备在周末有时间就写 1-2 个回答,当然为了严谨,我会尽可能通过源码和案例来辅证。
每日一问 自定义 ViewGroup 的时候,关于 LayoutParams 有哪些注意事项?
相信大家都看过非常多自定义控件的文章,我估计几乎所有人都能说出自定义的步骤。
但是实际上有很多细节,大家可能没有太关注。
这个问题,小缘的回答我就不贴了,回答的很全面了,可以去网站查看。
1. 复写 generateLayoutParams(AttributeSet attrs)
自定义 ViewGroup,它肯定有它特有的 layout 方式,很多时候需要依赖一些属性来完成,我们平时在xml 声明的 layout_开头的属性,其实都是该控件父控件的中 LayoutParams 所支持的属性。
例如:
LinearLayout.LayoutParams 的weight,gravity。
RelativeLayout.LayoutParam的各种 rules。
下面是 LinearLayout的源码:
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LinearLayout.LayoutParams(getContext(), attrs);
}
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray a =
c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout_Layout);
weight = a.getFloat(com.android.internal.R.styleable.LinearLayout_Layout_layout_weight, 0);
gravity = a.getInt(com.android.internal.R.styleable.LinearLayout_Layout_layout_gravity, -1);
a.recycle();
}
ok,这个大家应该都清楚。
2. 复写generateDefaultLayoutParams()
我们先写个 demo:
先声明一个 LinearLayout:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:id="@+id/ll_container"
android:orientation="vertical"
android:layout_height="match_parent"
tools:context=".LayoutParamsActivity">
</LinearLayout>
然后动态 add 一个 View:
LinearLayout llContainer = findViewById(R.id.ll_container);
TextView tv = new TextView(this);
tv.setText("哈哈哈");
tv.setTextColor(Color.WHITE);
tv.setBackgroundColor(Color.BLACK);
llContainer.addView(tv);
看一下效果图:
可以看到我们的 TextView 宽度:match_parent,高度 wrap_content 的一个状态。
这里我们修改一下 LinearLayout 的 orientation为horizontal:
<LinearLayout
android:id="@+id/ll_container"
android:orientation="horizontal">
</LinearLayout>
其他不不动,再运行一下:
可以看到,我们的 TextView 的宽高状态发生了变化,变成了 wrap_content,wrap_content。
为什么会这样呢?
这里就引出第二个方法了,看一下 LinearLayout 的源码:
protected LayoutParams generateDefaultLayoutParams() {
if (mOrientation == HORIZONTAL) {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
} else if (mOrientation == VERTICAL) {
return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}
return null;
}
可以看到generateDefaultLayoutParams()可以用于动态添加 View时,设置默认的LayoutParams 对象。
比如你在自定义一个 ViewGroup 的时候,是不是要思考下,哪种状态应该默认呢?是不是应该根据某个属性动态设置状态?
咱们继续。
3. 复写 generateLayoutParams(ViewGroup.LayoutParams lp)
这个方法就经常有同学忘了复写了。
有什么作用呢?
我从网上找了个 FlowLayout 的实现,大家不用在意细节,不过为了大家能够测试,我还是贴一下完整代码,大家阅读可以直接跳过:
public class FlowLayout extends ViewGroup {
private static final int[] LL = new int[]{android.R.attr.maxLines};
private List<List<View>> mAllViews = new ArrayList<List<View>>();
private List<Integer> mLineHeight = new ArrayList<Integer>();
private int mMaxLine = Integer.MAX_VALUE;
public FlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, LL);
mMaxLine = a.getInt(0, Integer.MAX_VALUE);
a.recycle();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mAllViews.clear();
mLineHeight.clear();
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
int height = 0;
int lineWidth = 0;
int lineHeight = 0;
int cCount = getChildCount();
List<View> lineViews = new ArrayList<View>();
for (int i = 0; i < cCount; i++) {
View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
MarginLayoutParams lp = (MarginLayoutParams) child
.getLayoutParams();
int childWidth = child.getMeasuredWidth() + lp.leftMargin
+ lp.rightMargin;
int childHeight = child.getMeasuredHeight() + lp.topMargin
+ lp.bottomMargin;
if (lineWidth + childWidth > sizeWidth - (getPaddingLeft() + getPaddingRight())) {
// new line
lineWidth = childWidth;
height += lineHeight;
mLineHeight.add(lineHeight);
lineHeight = childHeight;
mAllViews.add(lineViews);
lineViews = new ArrayList<>();
lineViews.add(child);
} else {
lineWidth += childWidth;
lineHeight = Math.max(lineHeight, childHeight);
lineViews.add(child);
}
if (i == cCount - 1) {
height += lineHeight;
mLineHeight.add(lineHeight);
mAllViews.add(lineViews);
}
}
if (mMaxLine < mLineHeight.size()) {
height = getMaxLineHeight();
}
setMeasuredDimension(sizeWidth, (modeHeight == MeasureSpec.EXACTLY) ? sizeHeight
: height + getPaddingTop() + getPaddingBottom());
}
private int getMaxLineHeight() {
int height = 0;
for (int i = 0; i < mMaxLine; i++) {
height += mLineHeight.get(i);
}
return height;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int left = getPaddingLeft();
int top = getPaddingTop();
// 得到总行数
int lineNums = mAllViews.size();
for (int i = 0; i < lineNums; i++) {
// 每一行的所有的views
List<View> lineViews = mAllViews.get(i);
// 当前行的最大高度
int lineHeight = mLineHeight.get(i);
// 遍历当前行所有的View
for (int j = 0; j < lineViews.size(); j++) {
View child = lineViews.get(j);
if (child.getVisibility() == View.GONE) {
continue;
}
MarginLayoutParams lp = (MarginLayoutParams) child
.getLayoutParams();
//计算childView的left,top,right,bottom
int lc = left + lp.leftMargin;
int tc = top + lp.topMargin;
int rc = lc + child.getMeasuredWidth();
int bc = tc + child.getMeasuredHeight();
child.layout(lc, tc, rc, bc);
left += child.getMeasuredWidth() + lp.rightMargin
+ lp.leftMargin;
}
left = getPaddingLeft();
top += lineHeight;
}
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
}
我们声明一下,写一小段简单的代码:
FlowLayout flowLayout = findViewById(R.id.flowlayout);
TextView tv = new TextView(this);
tv.setText("哈哈哈");
tv.setTextColor(Color.WHITE);
tv.setBackgroundColor(Color.BLACK);
tv.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
flowLayout.addView(tv);
运行:
java.lang.ClassCastException: android.view.ViewGroup$LayoutParams cannot be cast to android.view.ViewGroup$MarginLayoutParams
at com.imooc.imooc_flowlayout.FlowLayout.onMeasure(FlowLayout.java:62)
at android.view.View.measure(View.java:16497)
会出现一个强转异常。
原因是,这里 TextView 设置的 LayoutParams 是 ViewGroup,而我们在 onMeasure 的时候,需要用过 child.getLayoutParams,直接转为 MarginLayoutParams 了。
那么是我们 onMeasure 的代码写的不规范吗?需要先 check 一下再强转?
相同的代码,我们把容器从 FlowLayout修改为 LinearLayout,再次运行:
并没有发生异常,很符合我们的预期。
是 LinearLayout 的 onMeasure 里面有 check 操作?
我们看一眼 LinearLayout 的 onMeasure 的源码:
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
// ...
final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
}
可以看到 LinearLayout 的内部也是直接强转的,为什么它没事。
这时候,你可能想到了这一节的标题,一定是要重写generateLayoutParams(ViewGroup.LayoutParams lp)方法。
那我们先在 FlowLayout 中重写下:
/**
* add view cast
*
* @param p
* @return
*/
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
如果传入的为 ViewGroup.LayoutParams 的,我们手动转位 MarginLayoutParams。
修改代码为 FlowLayout.addView,运行一哈。
依然崩溃...
为什么呢?
接下来我们要去看源码了。
4. 源码之旅
直接点击 addView,ViewGroup.addView
public void addView(View child) {
addView(child, -1);
}
public void addView(View child, int index) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = generateDefaultLayoutParams();
if (params == null) {
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
}
}
addView(child, index, params);
}
注意看,如果 child.getLayoutParams 为 null,则会调用 generateDefaulyLayouyParams 方法,对应我们的第二节标题。
咱们继续:
public void addView(View child, int index, LayoutParams params) {
requestLayout();
invalidate(true);
addViewInner(child, index, params, false);
}
private void addViewInner(View child, int index, LayoutParams params,
boolean preventRequestLayout) {
if (!checkLayoutParams(params)) {
params = generateLayoutParams(params);
}
if (preventRequestLayout) {
child.mLayoutParams = params;
} else {
child.setLayoutParams(params);
}
//...
}
好像发现了,当:
if (!checkLayoutParams(params)) {
params = generateLayoutParams(params);
}
原来,调用 generateLayout(params)方法,还需要一个条件 checkLayoutParams 返回 false。
那么我们刚才失效,说明这个方法默认返回 true 咯。
看第四个方法吧。
5. 复写checkLayoutParams(LayoutParams p)
默认实现:
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p != null;
}
默认不为 null,就返回 true了。
那么我们FlowLayout需要的是 MarginLayoutParams,我们修改一下:
protected boolean checkLayoutParams(LayoutParams p) {
return p instanceof MarginLayoutParams;
}
再次运行,终于符合预期了。
好了,以后遇到 LayoutParams 造成的 ClassCastException,可能不是你写代码的问题,而是那个封装控件的人,有些细节没有考虑到。
有了这个判断,我们就可以为所欲为,随意 setLayoutParams 吗?
我们再修改下代码:
FlowLayout flowLayout = findViewById(R.id.flowlayout);
TextView tv = new TextView(this);
tv.setText("哈哈哈");
tv.setTextColor(Color.WHITE);
tv.setBackgroundColor(Color.BLACK);
flowLayout.addView(tv);
tv.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
又崩溃咯:
java.lang.ClassCastException: android.view.ViewGroup$LayoutParams cannot be cast to android.view.ViewGroup$MarginLayoutParams
at com.imooc.imooc_flowlayout.FlowLayout.onMeasure(FlowLayout.java:62)
原因很简单了,addView 虽然父控件会帮你转化一下,但是一旦 addView 之后,父控件也没办法一直照顾着你...
好了,所以,当你下次自定义 ViewGroup的时候,需要考虑复写:
generateLayoutParams(AttributeSet attrs)
generateDefaultLayoutParams()
generateLayoutParams(LayoutParams p)
checkLayoutParams(LayoutParams p)
而且我们也从源码层看到这四个方法的作用。
最后我们也告诉了你,即使有这些方法,也不要随便 setLayoutParams...注意类型。
好了,以后就按照本文的文风给大家解答每日一问,会通过案例,源码保证回答的严谨性,有什么意见欢迎留言。
推荐阅读:
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!